// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object') {
    // CommonJS
    mod(require('../../lib/codemirror'));
  } else if (typeof define == 'function' && define.amd) {
    // AMD
    define(['../../lib/codemirror'], mod);
  } else {
    // Plain browser env
    mod(CodeMirror);
  }
})(function(CodeMirror) {
  'use strict';

  var TOKEN_STYLES = {
    addition: 'positive',
    attributes: 'attribute',
    bold: 'strong',
    cite: 'keyword',
    code: 'atom',
    definitionList: 'number',
    deletion: 'negative',
    div: 'punctuation',
    em: 'em',
    footnote: 'variable',
    footCite: 'qualifier',
    header: 'header',
    html: 'comment',
    image: 'string',
    italic: 'em',
    link: 'link',
    linkDefinition: 'link',
    list1: 'variable-2',
    list2: 'variable-3',
    list3: 'keyword',
    notextile: 'string-2',
    pre: 'operator',
    p: 'property',
    quote: 'bracket',
    span: 'quote',
    specialChar: 'tag',
    strong: 'strong',
    sub: 'builtin',
    sup: 'builtin',
    table: 'variable-3',
    tableHeading: 'operator',
  };

  function startNewLine(stream, state) {
    state.mode = Modes.newLayout;
    state.tableHeading = false;

    if (
      state.layoutType === 'definitionList' &&
      state.spanningLayout &&
      stream.match(RE('definitionListEnd'), false)
    )
      state.spanningLayout = false;
  }

  function handlePhraseModifier(stream, state, ch) {
    if (ch === '_') {
      if (stream.eat('_'))
        return togglePhraseModifier(stream, state, 'italic', /__/, 2);
      else return togglePhraseModifier(stream, state, 'em', /_/, 1);
    }

    if (ch === '*') {
      if (stream.eat('*')) {
        return togglePhraseModifier(stream, state, 'bold', /\*\*/, 2);
      }
      return togglePhraseModifier(stream, state, 'strong', /\*/, 1);
    }

    if (ch === '[') {
      if (stream.match(/\d+\]/)) state.footCite = true;
      return tokenStyles(state);
    }

    if (ch === '(') {
      var spec = stream.match(/^(r|tm|c)\)/);
      if (spec) return tokenStylesWith(state, TOKEN_STYLES.specialChar);
    }

    if (ch === '<' && stream.match(/(\w+)[^>]+>[^<]+<\/\1>/))
      return tokenStylesWith(state, TOKEN_STYLES.html);

    if (ch === '?' && stream.eat('?'))
      return togglePhraseModifier(stream, state, 'cite', /\?\?/, 2);

    if (ch === '=' && stream.eat('='))
      return togglePhraseModifier(stream, state, 'notextile', /==/, 2);

    if (ch === '-' && !stream.eat('-'))
      return togglePhraseModifier(stream, state, 'deletion', /-/, 1);

    if (ch === '+')
      return togglePhraseModifier(stream, state, 'addition', /\+/, 1);

    if (ch === '~') return togglePhraseModifier(stream, state, 'sub', /~/, 1);

    if (ch === '^') return togglePhraseModifier(stream, state, 'sup', /\^/, 1);

    if (ch === '%') return togglePhraseModifier(stream, state, 'span', /%/, 1);

    if (ch === '@') return togglePhraseModifier(stream, state, 'code', /@/, 1);

    if (ch === '!') {
      var type = togglePhraseModifier(
        stream,
        state,
        'image',
        /(?:\([^\)]+\))?!/,
        1,
      );
      stream.match(/^:\S+/); // optional Url portion
      return type;
    }
    return tokenStyles(state);
  }

  function togglePhraseModifier(
    stream,
    state,
    phraseModifier,
    closeRE,
    openSize,
  ) {
    var charBefore =
      stream.pos > openSize
        ? stream.string.charAt(stream.pos - openSize - 1)
        : null;
    var charAfter = stream.peek();
    if (state[phraseModifier]) {
      if (
        (!charAfter || /\W/.test(charAfter)) &&
        charBefore &&
        /\S/.test(charBefore)
      ) {
        var type = tokenStyles(state);
        state[phraseModifier] = false;
        return type;
      }
    } else if (
      (!charBefore || /\W/.test(charBefore)) &&
      charAfter &&
      /\S/.test(charAfter) &&
      stream.match(new RegExp('^.*\\S' + closeRE.source + '(?:\\W|$)'), false)
    ) {
      state[phraseModifier] = true;
      state.mode = Modes.attributes;
    }
    return tokenStyles(state);
  }

  function tokenStyles(state) {
    var disabled = textileDisabled(state);
    if (disabled) return disabled;

    var styles = [];
    if (state.layoutType) styles.push(TOKEN_STYLES[state.layoutType]);

    styles = styles.concat(
      activeStyles(
        state,
        'addition',
        'bold',
        'cite',
        'code',
        'deletion',
        'em',
        'footCite',
        'image',
        'italic',
        'link',
        'span',
        'strong',
        'sub',
        'sup',
        'table',
        'tableHeading',
      ),
    );

    if (state.layoutType === 'header')
      styles.push(TOKEN_STYLES.header + '-' + state.header);

    return styles.length ? styles.join(' ') : null;
  }

  function textileDisabled(state) {
    var type = state.layoutType;

    switch (type) {
      case 'notextile':
      case 'code':
      case 'pre':
        return TOKEN_STYLES[type];
      default:
        if (state.notextile)
          return (
            TOKEN_STYLES.notextile + (type ? ' ' + TOKEN_STYLES[type] : '')
          );
        return null;
    }
  }

  function tokenStylesWith(state, extraStyles) {
    var disabled = textileDisabled(state);
    if (disabled) return disabled;

    var type = tokenStyles(state);
    if (extraStyles) return type ? type + ' ' + extraStyles : extraStyles;
    else return type;
  }

  function activeStyles(state) {
    var styles = [];
    for (var i = 1; i < arguments.length; ++i) {
      if (state[arguments[i]]) styles.push(TOKEN_STYLES[arguments[i]]);
    }
    return styles;
  }

  function blankLine(state) {
    var spanningLayout = state.spanningLayout,
      type = state.layoutType;

    for (var key in state) if (state.hasOwnProperty(key)) delete state[key];

    state.mode = Modes.newLayout;
    if (spanningLayout) {
      state.layoutType = type;
      state.spanningLayout = true;
    }
  }

  var REs = {
    cache: {},
    single: {
      bc: 'bc',
      bq: 'bq',
      definitionList: /- .*?:=+/,
      definitionListEnd: /.*=:\s*$/,
      div: 'div',
      drawTable: /\|.*\|/,
      foot: /fn\d+/,
      header: /h[1-6]/,
      html: /\s*<(?:\/)?(\w+)(?:[^>]+)?>(?:[^<]+<\/\1>)?/,
      link: /[^"]+":\S/,
      linkDefinition: /\[[^\s\]]+\]\S+/,
      list: /(?:#+|\*+)/,
      notextile: 'notextile',
      para: 'p',
      pre: 'pre',
      table: 'table',
      tableCellAttributes: /[\/\\]\d+/,
      tableHeading: /\|_\./,
      tableText: /[^"_\*\[\(\?\+~\^%@|-]+/,
      text: /[^!"_=\*\[\(<\?\+~\^%@-]+/,
    },
    attributes: {
      align: /(?:<>|<|>|=)/,
      selector: /\([^\(][^\)]+\)/,
      lang: /\[[^\[\]]+\]/,
      pad: /(?:\(+|\)+){1,2}/,
      css: /\{[^\}]+\}/,
    },
    createRe: function(name) {
      switch (name) {
        case 'drawTable':
          return REs.makeRe('^', REs.single.drawTable, '$');
        case 'html':
          return REs.makeRe(
            '^',
            REs.single.html,
            '(?:',
            REs.single.html,
            ')*',
            '$',
          );
        case 'linkDefinition':
          return REs.makeRe('^', REs.single.linkDefinition, '$');
        case 'listLayout':
          return REs.makeRe('^', REs.single.list, RE('allAttributes'), '*\\s+');
        case 'tableCellAttributes':
          return REs.makeRe(
            '^',
            REs.choiceRe(REs.single.tableCellAttributes, RE('allAttributes')),
            '+\\.',
          );
        case 'type':
          return REs.makeRe('^', RE('allTypes'));
        case 'typeLayout':
          return REs.makeRe(
            '^',
            RE('allTypes'),
            RE('allAttributes'),
            '*\\.\\.?',
            '(\\s+|$)',
          );
        case 'attributes':
          return REs.makeRe('^', RE('allAttributes'), '+');

        case 'allTypes':
          return REs.choiceRe(
            REs.single.div,
            REs.single.foot,
            REs.single.header,
            REs.single.bc,
            REs.single.bq,
            REs.single.notextile,
            REs.single.pre,
            REs.single.table,
            REs.single.para,
          );

        case 'allAttributes':
          return REs.choiceRe(
            REs.attributes.selector,
            REs.attributes.css,
            REs.attributes.lang,
            REs.attributes.align,
            REs.attributes.pad,
          );

        default:
          return REs.makeRe('^', REs.single[name]);
      }
    },
    makeRe: function() {
      var pattern = '';
      for (var i = 0; i < arguments.length; ++i) {
        var arg = arguments[i];
        pattern += typeof arg === 'string' ? arg : arg.source;
      }
      return new RegExp(pattern);
    },
    choiceRe: function() {
      var parts = [arguments[0]];
      for (var i = 1; i < arguments.length; ++i) {
        parts[i * 2 - 1] = '|';
        parts[i * 2] = arguments[i];
      }

      parts.unshift('(?:');
      parts.push(')');
      return REs.makeRe.apply(null, parts);
    },
  };

  function RE(name) {
    return REs.cache[name] || (REs.cache[name] = REs.createRe(name));
  }

  var Modes = {
    newLayout: function(stream, state) {
      if (stream.match(RE('typeLayout'), false)) {
        state.spanningLayout = false;
        return (state.mode = Modes.blockType)(stream, state);
      }
      var newMode;
      if (!textileDisabled(state)) {
        if (stream.match(RE('listLayout'), false)) newMode = Modes.list;
        else if (stream.match(RE('drawTable'), false)) newMode = Modes.table;
        else if (stream.match(RE('linkDefinition'), false))
          newMode = Modes.linkDefinition;
        else if (stream.match(RE('definitionList')))
          newMode = Modes.definitionList;
        else if (stream.match(RE('html'), false)) newMode = Modes.html;
      }
      return (state.mode = newMode || Modes.text)(stream, state);
    },

    blockType: function(stream, state) {
      var match, type;
      state.layoutType = null;

      if ((match = stream.match(RE('type')))) type = match[0];
      else return (state.mode = Modes.text)(stream, state);

      if ((match = type.match(RE('header')))) {
        state.layoutType = 'header';
        state.header = parseInt(match[0][1]);
      } else if (type.match(RE('bq'))) {
        state.layoutType = 'quote';
      } else if (type.match(RE('bc'))) {
        state.layoutType = 'code';
      } else if (type.match(RE('foot'))) {
        state.layoutType = 'footnote';
      } else if (type.match(RE('notextile'))) {
        state.layoutType = 'notextile';
      } else if (type.match(RE('pre'))) {
        state.layoutType = 'pre';
      } else if (type.match(RE('div'))) {
        state.layoutType = 'div';
      } else if (type.match(RE('table'))) {
        state.layoutType = 'table';
      }

      state.mode = Modes.attributes;
      return tokenStyles(state);
    },

    text: function(stream, state) {
      if (stream.match(RE('text'))) return tokenStyles(state);

      var ch = stream.next();
      if (ch === '"') return (state.mode = Modes.link)(stream, state);
      return handlePhraseModifier(stream, state, ch);
    },

    attributes: function(stream, state) {
      state.mode = Modes.layoutLength;

      if (stream.match(RE('attributes')))
        return tokenStylesWith(state, TOKEN_STYLES.attributes);
      else return tokenStyles(state);
    },

    layoutLength: function(stream, state) {
      if (stream.eat('.') && stream.eat('.')) state.spanningLayout = true;

      state.mode = Modes.text;
      return tokenStyles(state);
    },

    list: function(stream, state) {
      var match = stream.match(RE('list'));
      state.listDepth = match[0].length;
      var listMod = (state.listDepth - 1) % 3;
      if (!listMod) state.layoutType = 'list1';
      else if (listMod === 1) state.layoutType = 'list2';
      else state.layoutType = 'list3';

      state.mode = Modes.attributes;
      return tokenStyles(state);
    },

    link: function(stream, state) {
      state.mode = Modes.text;
      if (stream.match(RE('link'))) {
        stream.match(/\S+/);
        return tokenStylesWith(state, TOKEN_STYLES.link);
      }
      return tokenStyles(state);
    },

    linkDefinition: function(stream, state) {
      stream.skipToEnd();
      return tokenStylesWith(state, TOKEN_STYLES.linkDefinition);
    },

    definitionList: function(stream, state) {
      stream.match(RE('definitionList'));

      state.layoutType = 'definitionList';

      if (stream.match(/\s*$/)) state.spanningLayout = true;
      else state.mode = Modes.attributes;

      return tokenStyles(state);
    },

    html: function(stream, state) {
      stream.skipToEnd();
      return tokenStylesWith(state, TOKEN_STYLES.html);
    },

    table: function(stream, state) {
      state.layoutType = 'table';
      return (state.mode = Modes.tableCell)(stream, state);
    },

    tableCell: function(stream, state) {
      if (stream.match(RE('tableHeading'))) state.tableHeading = true;
      else stream.eat('|');

      state.mode = Modes.tableCellAttributes;
      return tokenStyles(state);
    },

    tableCellAttributes: function(stream, state) {
      state.mode = Modes.tableText;

      if (stream.match(RE('tableCellAttributes')))
        return tokenStylesWith(state, TOKEN_STYLES.attributes);
      else return tokenStyles(state);
    },

    tableText: function(stream, state) {
      if (stream.match(RE('tableText'))) return tokenStyles(state);

      if (stream.peek() === '|') {
        // end of cell
        state.mode = Modes.tableCell;
        return tokenStyles(state);
      }
      return handlePhraseModifier(stream, state, stream.next());
    },
  };

  CodeMirror.defineMode('textile', function() {
    return {
      startState: function() {
        return { mode: Modes.newLayout };
      },
      token: function(stream, state) {
        if (stream.sol()) startNewLine(stream, state);
        return state.mode(stream, state);
      },
      blankLine: blankLine,
    };
  });

  CodeMirror.defineMIME('text/x-textile', 'textile');
});
